import { CB, deferThrow } from 'vest-utils';
import { Isolate } from 'vestjs-runtime';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { dummyTest } from '../testUtils/testDummy';
import * as vest from '../vest';

vi.mock('vest-utils', async () => {
  const vu = await vi.importActual('vest-utils');
  return {
    ...vu,
    deferThrow: vi.fn(),
  };
});

describe('isolate', () => {
  let firstRun = true;

  beforeEach(() => {
    firstRun = true;
  });

  describe('Base behavior', () => {
    it("Should throw an error if the callback isn't a function", () => {
      // @ts-ignore - testing bad input
      expect(() => Isolate.create('xx', {}, 'not a function')).toThrow();
    });

    it('Should retain test results between runs', () => {
      const f1 = vi.fn(() => false);
      const f2 = vi.fn(() => false);
      const suite = genSuite(() => {
        vest.skipWhen(!firstRun, () => {
          vest.test('f1', f1);
          vest.test('f2', f2);
        });
      });

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(f1).toHaveBeenCalledTimes(1);
      expect(f2).toHaveBeenCalledTimes(1);
      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(f1).toHaveBeenCalledTimes(1);
      expect(f2).toHaveBeenCalledTimes(1);
    });
  });

  describe('When order changes within the isolate', () => {
    it('Should contain test order changes within the isolate', () => {
      const suite = genSuite(() => {
        dummyTest.failing('f1');

        vest.group(() => {
          dummyTest.failing('f2');
          if (!firstRun) {
            dummyTest.failing('f3');
            dummyTest.failing('f4');
          }
        });

        vest.skipWhen(!firstRun, () => {
          dummyTest.failing('f5');
        });
      });

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(suite.get().hasErrors('f3')).toBe(false);
      expect(suite.get().hasErrors('f4')).toBe(false);
      expect(suite.get().hasErrors('f5')).toBe(true);
      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeDefined();
      expect(suite.get().tests.f3).toBeUndefined();
      expect(suite.get().tests.f4).toBeUndefined();
      expect(suite.get().tests.f5).toBeDefined();

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(suite.get().hasErrors('f3')).toBe(true);
      expect(suite.get().hasErrors('f4')).toBe(true);

      // without "isolate" this assertion would fail
      // because the test would have been overwritten
      expect(suite.get().hasErrors('f5')).toBe(true);

      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeDefined();
      expect(suite.get().tests.f3).toBeDefined();
      expect(suite.get().tests.f4).toBeDefined();
      expect(suite.get().tests.f5).toBeDefined();
    });

    it('Should only retain the state of the unmoved state before the order index', () => {
      const suite = genSuite(() => {
        vest.group(() => {
          vest.skipWhen(!firstRun, () => {
            dummyTest.failing('f1');
          });
          if (!firstRun) {
            dummyTest.failing('f2');
          }
          vest.skipWhen(!firstRun, () => {
            dummyTest.failing('f3');
          });
        });

        suite.run();
        expect(suite.get().hasErrors('f1')).toBe(true);
        expect(suite.get().hasErrors('f2')).toBe(false);
        expect(suite.get().hasErrors('f3')).toBe(true);
        expect(suite.get().tests.f1).toBeDefined();
        expect(suite.get().tests.f2).toBeUndefined();
        expect(suite.get().tests.f3).toBeDefined();

        suite.run();
        expect(suite.get().hasErrors('f1')).toBe(true);
        expect(suite.get().hasErrors('f2')).toBe(true);
        expect(suite.get().hasErrors('f3')).toBe(false);
        expect(suite.get().tests.f1).toBeDefined();
        expect(suite.get().tests.f2).toBeDefined();
        expect(suite.get().tests.f3).toBeDefined();
      });
    });
  });

  describe('When test order changes before the isolate opens', () => {
    it('Should clean up follow up tests. Reregister', () => {
      const suite = genSuite(() => {
        dummyTest.failing('f1');
        if (!firstRun) {
          dummyTest.failing('f6');
        }

        // this way we can tell if the state is kept or discarded.
        // if the state is kept, they should be invalid. Otherwise
        // they should be untested.
        vest.skipWhen(!firstRun, () => {
          vest.group(() => {
            dummyTest.failing('f2');
            dummyTest.failing('f3');
            dummyTest.failing('f4');
          });

          dummyTest.failing('f5');
        });
      });

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(suite.get().hasErrors('f3')).toBe(true);
      expect(suite.get().hasErrors('f4')).toBe(true);
      expect(suite.get().hasErrors('f5')).toBe(true);
      expect(suite.get().hasErrors('f6')).toBe(false);
      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeDefined();
      expect(suite.get().tests.f3).toBeDefined();
      expect(suite.get().tests.f4).toBeDefined();
      expect(suite.get().tests.f5).toBeDefined();
      expect(suite.get().tests.f6).toBeUndefined();

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(false);
      expect(suite.get().hasErrors('f3')).toBe(false);
      expect(suite.get().hasErrors('f4')).toBe(false);
      expect(suite.get().hasErrors('f5')).toBe(false);
      expect(suite.get().hasErrors('f6')).toBe(true);
      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeDefined();
      expect(suite.get().tests.f3).toBeDefined();
      expect(suite.get().tests.f4).toBeDefined();
      expect(suite.get().tests.f5).toBeDefined();
      expect(suite.get().tests.f6).toBeDefined();
    });
  });

  describe('When an incorrect isolate is encountered', () => {
    it('Should replace isolate completely', () => {
      const suite = genSuite(() => {
        if (firstRun) {
          vest.group(() => {
            dummyTest.failing('f1');
          });
        } else {
          vest.group(() => {
            dummyTest.failing('f2');
          });
        }
      });

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(false);
      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeUndefined();
      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(false);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(suite.get().tests.f1).toBeUndefined();
      expect(suite.get().tests.f2).toBeDefined();
    });
  });

  describe('When an isolate is present when a test was expected', () => {
    it('Should erase test history, and re-register', () => {
      const suite = genSuite(() => {
        if (firstRun) {
          dummyTest.failing('f1');
        } else {
          vest.group(() => {
            dummyTest.failing('f2');
          });
        }
      });

      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(true);
      expect(suite.get().hasErrors('f2')).toBe(false);
      expect(suite.get().tests.f1).toBeDefined();
      expect(suite.get().tests.f2).toBeUndefined();
      suite.run();
      expect(suite.get().hasErrors('f1')).toBe(false);
      expect(suite.get().hasErrors('f2')).toBe(true);
      expect(suite.get().tests.f1).toBeUndefined();
      expect(suite.get().tests.f2).toBeDefined();
    });

    describe('Errors', () => {
      it('should throw a deferred error when the tests are out of order', () => {
        const suite = genSuite(() => {
          vest.group(() => {
            dummyTest.failing(firstRun ? 'f1' : 'f2');
          });
        });

        suite.run();
        suite.run();
        expect(deferThrow).toHaveBeenCalledWith(
          expect.stringContaining(
            'Vest Critical Error: Tests called in different order than previous run',
          ),
        );
      });
    });
  });

  function genSuite(cb: CB) {
    return vest.create(() => {
      cb();
      firstRun = false;
    });
  }
});
